package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.LinearFunction;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointAndSlope;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.shorts.ShortList;

import java.awt.Color;
import java.awt.Font;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.jfree.util.ShapeUtilities;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;

public class PicoShapes {
  
  public static final List<Color> defaultCurrentAndSegmentColors = ImmutableList.of(
  Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA, Color.ORANGE,
  Color.DARK_GRAY, Color.PINK);

	public static List<Shape> drawArrayAndJunctionPoints(IntList junctionPoints, ShortList a) {
		List<Shape> zCrosses = new ArrayList<>();
		for (int i = 0; i < junctionPoints.size(); ++i) {
			Shape regularCross = ShapeUtilities.createTranslatedShape(ShapeUtilities
					.createRegularCross(5, 5), junctionPoints.getInt(i), a.getShort(junctionPoints.getInt(i)));
			zCrosses.add(regularCross);
		}
		List<Shape> aShapes = DirectionData.draw(a);
		List<Shape> atShapes = new ArrayList<>();
		atShapes.addAll(aShapes);
		atShapes.addAll(zCrosses);
		return atShapes;
	}

	public static List<Shape> drawApproachContactPointAndSlope(
			ContactPointAndSlope approachContactPointAndSlope,
			DirectionData approach, double zLsbToMetresFactor, double svgDefScale) {
		LinearFunction approachEndLinearFunction = approachContactPointAndSlope.baseline();
        Shape approachEndLine = approach.timeInstantCount() == 0 ? null : new Line2D.Double(
            approachContactPointAndSlope.zLsb,
            approachEndLinearFunction.evaluate(approachContactPointAndSlope.zLsb) * svgDefScale,
            approach.z(approach.timeInstantCount() - 1),
            approachEndLinearFunction.evaluate(approach.z(approach.timeInstantCount() - 1)) * svgDefScale);
		int m = 10;
		List<Shape> cross = PicoShapes.drawCross(
				approachContactPointAndSlope.zLsb, approachContactPointAndSlope.defLsb * svgDefScale, m);
		List<Shape> text = PicoShapes.drawHorizontalTextRightOf(
			PicoShapes.standardFont,
			PicoShapes.standardFontRenderContext,
			String.format(Locale.ROOT, "%.3f nm", approachContactPointAndSlope.zLsb
				* zLsbToMetresFactor * 1e9), approachContactPointAndSlope.zLsb + contactPointTextSpacer,
			approachContactPointAndSlope.defLsb * svgDefScale,
			approachContactPointAndSlope.slopeZLsbDefLsb * svgDefScale);
		List<Shape> result = new ArrayList<>();
		result.addAll(cross);
		result.addAll(text);
		if (approachEndLine != null) {
		    result.add(approachEndLine);
		}
		return result;
	}

	public static Map<Color, List<Shape>> draw(RedundantPicoCurveInfo picoAnalysis, double svgDefScale) {
		List<Shape> approachLines = drawApproachContactPointAndSlope(
				picoAnalysis.approachContactPointAndSlope, picoAnalysis.plainPicoData.approach,
				picoAnalysis.plainPicoData.zLsbToMetresFactor, svgDefScale);
		PlainPicoData plainPicoData = picoAnalysis.plainPicoData;
		ZLsbDeflectionLsbPoint retractionContactPoint = picoAnalysis.retractionContactPoint;
		PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections = picoAnalysis.offsetAndDeflections;
		double endLineSlopeZLsbDefLsb = picoAnalysis.approachContactPointAndSlope.slopeZLsbDefLsb;
		Map<Color, List<Shape>> shapes = PicoShapes.draw2(plainPicoData, retractionContactPoint,
				offsetAndDeflections, endLineSlopeZLsbDefLsb,
				picoAnalysis.peakOrNots, picoAnalysis.wlcPeakForces(), picoAnalysis.kbtOverP, svgDefScale);
		Map<Color, List<Shape>> result = CommonThings.unionMapsOfLists(
			shapes, ImmutableMap.of(Color.BLUE, approachLines));
		return result;
	}

  public static Map<Color, List<Shape>> drawRetractionDataAndWlc(
    RedundantPicoCurveInfo picoAnalysis, double svgDefScale) {
		PlainPicoData plainPicoData = picoAnalysis.plainPicoData;
		ZLsbDeflectionLsbPoint retractionContactPoint = picoAnalysis.retractionContactPoint;
		PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections = picoAnalysis.offsetAndDeflections;
		double endLineSlopeZLsbDefLsb = picoAnalysis.approachContactPointAndSlope.slopeZLsbDefLsb;
		PeakOrNot[] peakOrNots = picoAnalysis.peakOrNots;
		List<Shape> wlcShapes = drawWlc(plainPicoData, retractionContactPoint,
				offsetAndDeflections, endLineSlopeZLsbDefLsb, peakOrNots, picoAnalysis.wlcPeakForces(),
				picoAnalysis.kbtOverP, svgDefScale);
    return ImmutableMap.of(
      Color.RED,
      (List<Shape>) ImmutableList.copyOf(Iterables.concat(
        plainPicoData.retraction.draw(svgDefScale), wlcShapes)));
	}

	public static Map<Color, List<Shape>> draw2(
		PlainPicoData plainPicoData, ZLsbDeflectionLsbPoint retractionContactPoint,
		PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections, double endLineSlopeZLsbDefLsb,
		PeakOrNot[] peakOrNots, double[] wlcPeakForces, double kbtOverP, double svgDefScale) {
		Map<Color, List<Shape>> plainShapes = plainPicoData.draw(svgDefScale);
		Map<Color, List<Shape>> nonPlainShapes = drawNonPlain(
      plainPicoData, retractionContactPoint, offsetAndDeflections, endLineSlopeZLsbDefLsb,
      peakOrNots, wlcPeakForces, kbtOverP, svgDefScale);
		return CommonThings.unionMapsOfLists(plainShapes, nonPlainShapes);
	}

  public static Map<Color, List<Shape>> drawNonPlain(
    PlainPicoData plainPicoData, ZLsbDeflectionLsbPoint retractionContactPoint,
    PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections, double endLineSlopeZLsbDefLsb,
    PeakOrNot[] peakOrNots, double[] wlcPeakForces, double kbtOverP, double svgDefScale) {
    Map<Color, List<Shape>> someShapes = draw(
			plainPicoData, retractionContactPoint, offsetAndDeflections, endLineSlopeZLsbDefLsb,
			peakOrNots, wlcPeakForces, kbtOverP, svgDefScale);
		List<Shape> retractionShapes = drawRetractionInfo(
      plainPicoData, retractionContactPoint, endLineSlopeZLsbDefLsb, svgDefScale);
		Map<Color, List<Shape>> nonPlainShapes = CommonThings.unionMapsOfLists(someShapes, ImmutableMap.of(Color.RED, retractionShapes));
    return nonPlainShapes;
  }

  public static List<Shape> drawRetractionInfo(
    PlainPicoData plainPicoData, ZLsbDeflectionLsbPoint retractionContactPoint,
    double endLineSlopeZLsbDefLsb, double svgDefScale) {
    int m = 10;
    List<Shape> retractionContactPointShapes = PicoShapes.drawCross(
        retractionContactPoint.zLsb, retractionContactPoint.deflectionLsb * svgDefScale, m);
    
    List<Shape> forceSegmentRelatedShapes = forceSegmentRelatedShapes(
      plainPicoData, retractionContactPoint, endLineSlopeZLsbDefLsb, svgDefScale);
    List<Shape> piezoSegmentRelatedShapes = piezoSegmentRelatedShapes(
      plainPicoData, retractionContactPoint, svgDefScale);
    short retractionLastZ = plainPicoData.retraction.z(plainPicoData.retraction
			.timeInstantCount() - 1);
		double retractionX0 = retractionContactPoint.zLsb;
		double retractionY0 = retractionContactPoint.deflectionLsb;
		LinearFunction retractionEndLinearFunction = retractionContactPoint
			.linearFunctionWithSlope(endLineSlopeZLsbDefLsb);
		Shape retractionEndLine = new Line2D.Double(
			retractionX0, retractionY0 * svgDefScale, retractionLastZ,
			retractionEndLinearFunction.evaluate(retractionLastZ) * svgDefScale);
		List<Shape> retractionText = drawHorizontalTextRightOf(
			standardFont,
			standardFontRenderContext,
			String.format(Locale.ROOT, "%.3f nm", retractionX0 * plainPicoData.zLsbToMetresFactor
				* 1e9), retractionX0 + contactPointTextSpacer, retractionY0 * svgDefScale,
				endLineSlopeZLsbDefLsb * svgDefScale);
		List<Shape> retractionShapes = new ArrayList<>(retractionText);
		retractionShapes.addAll(retractionContactPointShapes);
		retractionShapes.add(retractionEndLine);
		retractionShapes.addAll(forceSegmentRelatedShapes);
		retractionShapes.addAll(piezoSegmentRelatedShapes);
    return retractionShapes;
  }

	private static Map<Color, List<Shape>> draw(PlainPicoData plainPicoData,
			ZLsbDeflectionLsbPoint retractionContactPoint,
			PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections,
			double endLineSlopeZLsbDefLsb, PeakOrNot[] peakOrNots, double[] wlcPeakForces,
			double kbtOverP, double svgDefScale) {
    Preconditions.checkArgument(peakOrNots.length == wlcPeakForces.length);
    List<Shape> wlcShapes = drawWlc(
      plainPicoData, retractionContactPoint, offsetAndDeflections, endLineSlopeZLsbDefLsb,
      peakOrNots, wlcPeakForces, kbtOverP, svgDefScale);
		List<Shape> interpeaks = drawInterpeaks(
      plainPicoData, offsetAndDeflections, peakOrNots, svgDefScale);
		List<Shape> redPeakRelatedShapes = drawPeakShapes(
      plainPicoData, offsetAndDeflections, peakOrNots, svgDefScale);
		return ImmutableMap.<Color, List<Shape>> of(
			Color.BLACK, interpeaks, Color.RED, redPeakRelatedShapes, Color.MAGENTA, wlcShapes);
	}

  public static List<Shape> drawPeakShapes(
    PlainPicoData plainPicoData, PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections,
    PeakOrNot[] peakOrNots, double svgDefScale) {
    int ellipseHeight = 10;
    double ellipseWidth = ellipseHeight;
    Font font = new Font(Font.SERIF, Font.PLAIN, (int) (ellipseHeight * 1.1));
    List<Shape> redPeakRelatedShapes = new ArrayList<>();
		for (int i = 0; i < peakOrNots.length; ++i) {
			PeakOrNot peakOrNot = peakOrNots[i];
			if (peakOrNot.isPeak()) {
				int realIndex = offsetAndDeflections.offset + i;
				short centerX = plainPicoData.retraction.z(realIndex);
				short centerY = plainPicoData.retraction.deflectionAtIndex(realIndex);
				Shape ellipse = new Ellipse2D.Double(centerX - ellipseWidth / 2, (centerY
						- ellipseHeight / 2) * svgDefScale, ellipseWidth, ellipseHeight);
				redPeakRelatedShapes.add(ellipse);

				// 0 if contour length is not present
				double contourLength = peakOrNot.nonNegativeContourLengthMetres;
				String text = String.format(Locale.ROOT, "(%.1f nm, %.1f pN)", contourLength * 1e9,
						-offsetAndDeflections.horizontalDeflectionAfterContactPoint[i]
								* plainPicoData.deflectionLsbToNewtonsFactor() * 1e12);
				List<Shape> glyphList = PicoShapes.drawVerticalTextAbove(
						font, standardFontRenderContext,
						text, centerX, (centerY - ellipseHeight / 2 * 3) * svgDefScale);
				Preconditions.checkState(text.length() == glyphList.size());
				redPeakRelatedShapes.addAll(glyphList);

				int realStartPeakSlopeIndex = offsetAndDeflections.offset
						+ peakOrNot.startSlopeIndex;
				double startPeakSlopeX = plainPicoData.retraction.z(realStartPeakSlopeIndex);
				short startPeakSlopeY = plainPicoData.retraction
						.deflectionAtIndex(realStartPeakSlopeIndex);
				int m = 10;
				Shape horizontalLine = new Line2D.Double(startPeakSlopeX - m, startPeakSlopeY * svgDefScale,
						startPeakSlopeX + m, startPeakSlopeY * svgDefScale);
				redPeakRelatedShapes.add(horizontalLine);
			}
		}
    return redPeakRelatedShapes;
  }

  public static List<Shape> drawInterpeaks(
    PlainPicoData plainPicoData, PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections,
    PeakOrNot[] peakOrNots, double svgDefScale) {
    int ellipseHeight = 10;
    Font font = new Font(Font.SERIF, Font.PLAIN, (int) (ellipseHeight * 1.1));
    List<Shape> interpeaks = new ArrayList<>();
    for (int i = 0, index = -1; i < peakOrNots.length; ++i) {
      PeakOrNot peakOrNot = peakOrNots[i];
      if (peakOrNot.isPeak()) {
        if (index != -1) {
          int previousRealIndex = offsetAndDeflections.offset + index;
          short previousCenterX = plainPicoData.retraction.z(previousRealIndex);
          short previousCenterY = plainPicoData.retraction.deflectionAtIndex(previousRealIndex);
          double previousContourLength = peakOrNots[index].nonNegativeContourLengthMetres;
          
          int realIndex = offsetAndDeflections.offset + i;
          short centerX = plainPicoData.retraction.z(realIndex);
          short centerY = plainPicoData.retraction.deflectionAtIndex(realIndex);
          double contourLength = peakOrNot.nonNegativeContourLengthMetres;
          
          String text = String.format(Locale.ROOT, "+%.1f nm", (contourLength - previousContourLength) * 1e9);
          List<Shape> glyphList = PicoShapes.drawVerticalTextAbove(
            font, standardFontRenderContext, text, (previousCenterX + centerX) / 2.0,
            ((previousCenterY + centerY) / 2.0 - ellipseHeight / 2 * 3) * svgDefScale);
          interpeaks.addAll(glyphList);
        }
        index = i;
      }
    }
    return interpeaks;
  }

	public static List<Shape> drawWlc(PlainPicoData plainPicoData,
			ZLsbDeflectionLsbPoint retractionContactPoint,
			PicoDataAnalyser.OffsetAndDeflections offsetAndDeflections,
			double endLineSlopeZLsbDefLsb, PeakOrNot[] peakOrNots, double[] wlcPeakForces,
			double kbtOverP, double svgDefScale) {
		List<Shape> forceSegmentRelatedShapes = forceSegmentRelatedShapes(plainPicoData,
				retractionContactPoint, endLineSlopeZLsbDefLsb, svgDefScale);
		List<Shape> redWlcShapes = new ArrayList<>();
		redWlcShapes.addAll(forceSegmentRelatedShapes);
		double horizontalDeflection = PicoDataAnalyser
				.minHorizontalDeflectionOfPeakWithContourLength(peakOrNots, offsetAndDeflections);
		LinearFunction retractionEndLinearFunction = retractionContactPoint
				.linearFunctionWithSlope(endLineSlopeZLsbDefLsb);
		for (int i = peakOrNots.length; --i >= 0;) {
			PeakOrNot peakOrNot = peakOrNots[i];
			if (peakOrNot.isPeak()) {
				if (peakOrNot.hasContorLength()) {
					List<Point2D.Double> wlcPoints = PicoShapes.createWlcPoints(
							retractionContactPoint.zLsb, retractionEndLinearFunction,
							plainPicoData, horizontalDeflection,
							peakOrNot.nonNegativeContourLengthMetres, false, kbtOverP, svgDefScale);
					for (int j = 0; j < wlcPoints.size() - 1; ++j) {
						// Make it interrupted.
						if ((j & 0x1) == 0) {
							Shape segment = new Line2D.Double(wlcPoints.get(j), wlcPoints
									.get(j + 1));
							redWlcShapes.add(segment);
						}
					}
				}
			}
		}
		return redWlcShapes;
	}

	public static Font standardFont = new Font(Font.SERIF, Font.PLAIN, 15);
	public static FontRenderContext standardFontRenderContext = new FontRenderContext(null, true,
			true);
	private static final double contactPointTextSpacer = 40;
  public static final double DEFAULT_SVG_DEF_SCALE = 1.0;

  private static List<Shape> piezoSegmentRelatedShapes(PlainPicoData plainPicoData,
      ZLsbDeflectionLsbPoint retractionContactPoint, double svgDefScale) {
      double slope = 0;
      List<Shape> zSegmentRelatedShapes = new ArrayList<>();
      double tenNmInZLsb = 10e-9 / plainPicoData.zLsbToMetresFactor;
      // http://en.wikipedia.org/wiki/Trigonometric_identity#Compositions_of_trig_and_inverse_trig_functions
      double cosOfAtanOfSlope = 1 / (Math.sqrt(1 + slope * slope));
      double sinOfAtanOfSlope = slope * cosOfAtanOfSlope;
      double horizontalOffset = tenNmInZLsb / 2;
      double startDeflectionLsb = retractionContactPoint.deflectionLsb + horizontalOffset * sinOfAtanOfSlope;
      double endDeflectionLsb = startDeflectionLsb + tenNmInZLsb * sinOfAtanOfSlope;
      double zLsbSegmentStart = retractionContactPoint.zLsb + horizontalOffset * cosOfAtanOfSlope;
      double zLsbSegmentEnd = zLsbSegmentStart + tenNmInZLsb * cosOfAtanOfSlope;
      Shape forceSegment = new Line2D.Double(
          zLsbSegmentStart, startDeflectionLsb * svgDefScale, zLsbSegmentEnd, endDeflectionLsb * svgDefScale);
      zSegmentRelatedShapes.add(forceSegment);

      int m = 10;
      Shape smallVerticalLineStart = new Line2D.Double(
          zLsbSegmentStart + m * sinOfAtanOfSlope,
          (startDeflectionLsb * svgDefScale - m * cosOfAtanOfSlope),
          zLsbSegmentStart - m * sinOfAtanOfSlope,
          (startDeflectionLsb * svgDefScale + m * cosOfAtanOfSlope));
      zSegmentRelatedShapes.add(smallVerticalLineStart);
      Shape smallVerticalLineEnd = new Line2D.Double(
          zLsbSegmentEnd + m * sinOfAtanOfSlope,
          (endDeflectionLsb * svgDefScale - m * cosOfAtanOfSlope),
          zLsbSegmentEnd - m * sinOfAtanOfSlope,
          (endDeflectionLsb * svgDefScale + m * cosOfAtanOfSlope));
      zSegmentRelatedShapes.add(smallVerticalLineEnd);

      List<Shape> hundredPicoNewtonText = drawTextAbove(standardFont,
              standardFontRenderContext, "10 nm",
              (zLsbSegmentStart + zLsbSegmentEnd) / 2,
              ((startDeflectionLsb + endDeflectionLsb) / 2) * svgDefScale,
              slope);
      zSegmentRelatedShapes.addAll(hundredPicoNewtonText);
      return zSegmentRelatedShapes;
  }
    
  private static List<Shape> forceSegmentRelatedShapes(
    PlainPicoData plainPicoData, ZLsbDeflectionLsbPoint retractionContactPoint, double slope,
    double svgDefScale) {
    List<Shape> forceSegmentRelatedShapes = new ArrayList<>();
    double hundredPicoNewtonInDeflectionLsb = 100e-12
      / plainPicoData.deflectionLsbToNewtonsFactor() * svgDefScale;
    // http://en.wikipedia.org/wiki/Trigonometric_identity#Compositions_of_trig_and_inverse_trig_functions
    double cosOfAtanOfSlope = 1 / (Math.sqrt(1 + slope * slope));
    double sinOfAtanOfSlope = slope * cosOfAtanOfSlope;

    // double cosOfAtanOfSlopeOverSvgDefScale = 1 / (Math.sqrt(1 + slope /
    // svgDefScale * slope / svgDefScale));
    // double sinOfAtanOfSlopeOverSvgDefScale = slope / svgDefScale *
    // cosOfAtanOfSlopeOverSvgDefScale;

    double verticalOffset = -hundredPicoNewtonInDeflectionLsb / 2 * svgDefScale;
    double startZ = retractionContactPoint.zLsb - verticalOffset * sinOfAtanOfSlope;
    double endZ = startZ + hundredPicoNewtonInDeflectionLsb * sinOfAtanOfSlope;
    double forceSegmentStart = retractionContactPoint.deflectionLsb * svgDefScale + verticalOffset
      * cosOfAtanOfSlope;
    double forceSegmentEnd = forceSegmentStart - hundredPicoNewtonInDeflectionLsb
      * cosOfAtanOfSlope;
    Shape forceSegment = new Line2D.Double(startZ, forceSegmentStart, endZ, forceSegmentEnd);
    forceSegmentRelatedShapes.add(forceSegment);

    int m = 10;
    Shape smallHorizontalLineStart = new Line2D.Double(
      startZ - m * cosOfAtanOfSlope, (forceSegmentStart - m * sinOfAtanOfSlope), startZ + m
        * cosOfAtanOfSlope, (forceSegmentStart + m * sinOfAtanOfSlope));
    forceSegmentRelatedShapes.add(smallHorizontalLineStart);
    Shape smallHorizontalLineEnd = new Line2D.Double(
      endZ - m * cosOfAtanOfSlope, (forceSegmentEnd - m * sinOfAtanOfSlope), endZ + m
        * cosOfAtanOfSlope, (forceSegmentEnd + m * sinOfAtanOfSlope));
    forceSegmentRelatedShapes.add(smallHorizontalLineEnd);

    List<Shape> hundredPicoNewtonText = drawVerticalTextLeftOf(
      standardFont, standardFontRenderContext, "100 pN", (startZ + endZ) / 2,
      ((forceSegmentStart + forceSegmentEnd) / 2), slope);
    forceSegmentRelatedShapes.addAll(hundredPicoNewtonText);
    return forceSegmentRelatedShapes;
  }

	static List<Shape> drawVerticalTextLeftOf(Font font, FontRenderContext frc, String text,
			double x, double y, double slope) {
		Rectangle2D textBounds = new Rectangle2D.Double();
		Shape[] glyphs = drawTextAndUnionBounds(font, frc, text, textBounds);
		AffineTransform at = new AffineTransform();
		at.translate(x, y);
        at.rotate(Math.atan(slope));
        // Not sure if these calculations are accurate, but it works well with
        // 5, 20 and 100.
		at.translate(-textBounds.getHeight() / 2, textBounds.getWidth() / 2);
        at.rotate(-Math.PI / 2);
		for (int j = 0; j < glyphs.length; ++j) {
			glyphs[j] = at.createTransformedShape(glyphs[j]);
		}
		List<Shape> glyphList = Arrays.asList(glyphs);
		return glyphList;
	}

	static List<Shape> drawVerticalTextAbove(Font font, FontRenderContext frc, String text,
			double x, double y) {
		Rectangle2D textBounds = new Rectangle2D.Double();
		Shape[] glyphs = drawTextAndUnionBounds(font, frc, text, textBounds);
		// Not sure if these calculations are accurate, but it works well with
		// 5, 20 and 100.
		AffineTransform at = AffineTransform.getTranslateInstance(x, y);
		at.rotate(-Math.PI / 2);
		for (int j = 0; j < glyphs.length; ++j) {
			glyphs[j] = at.createTransformedShape(glyphs[j]);
		}
		List<Shape> glyphList = Arrays.asList(glyphs);
		return glyphList;
	}

	public static List<Shape> drawHorizontalTextRightOf(Font font, FontRenderContext frc,
			String text, double x, double y, double slope) {
		Rectangle2D textBounds = new Rectangle2D.Double();
		Shape[] glyphs = drawTextAndUnionBounds(font, frc, text, textBounds);
		// Not sure if these calculations are accurate, but it works well with
		// 5, 20 and 100.
		AffineTransform at = new AffineTransform();
		at.translate(x, y);
		at.rotate(Math.atan(slope));
		at.translate(0, textBounds.getHeight() / 2);
		for (int j = 0; j < glyphs.length; ++j) {
			glyphs[j] = at.createTransformedShape(glyphs[j]);
		}
		List<Shape> glyphList = Arrays.asList(glyphs);
		return glyphList;
	}

	public static List<Shape> drawTextBelow(Font font, FontRenderContext frc, String text, double x,
			double y) {
		Rectangle2D textBounds = new Rectangle2D.Double();
		Shape[] glyphs = drawTextAndUnionBounds(font, frc, text, textBounds);
		double xOffset = x - textBounds.getCenterX();
		double yOffset = y - textBounds.getHeight() / 2;
		AffineTransform at = AffineTransform.getTranslateInstance(xOffset, yOffset);
		for (int j = 0; j < glyphs.length; ++j) {
			glyphs[j] = at.createTransformedShape(glyphs[j]);
		}
		List<Shape> glyphList = Arrays.asList(glyphs);
		return glyphList;
	}

  public static List<Shape> drawTextAbove(
    Font font, FontRenderContext frc, String text, double x, double y, double slope) {
    Rectangle2D textBounds = new Rectangle2D.Double();
    Shape[] glyphs = drawTextAndUnionBounds(font, frc, text, textBounds);
    AffineTransform at = new AffineTransform();
    at.translate(x, y);
    at.rotate(Math.atan(slope));
    at.translate(-textBounds.getCenterX(), -0.5 * textBounds.getHeight());
    for (int j = 0; j < glyphs.length; ++j) {
      glyphs[j] = at.createTransformedShape(glyphs[j]);
    }
    List<Shape> glyphList = Arrays.asList(glyphs);
    return glyphList;
  }

	private static Shape[] drawTextAndUnionBounds(Font font, FontRenderContext frc, String text,
			Rectangle2D textBounds) {
		GlyphVector glyphVector = font.createGlyphVector(frc, text);
		Shape[] glyphs = new Shape[glyphVector.getNumGlyphs()];
		for (int j = 0; j < glyphs.length; ++j) {
			glyphs[j] = glyphVector.getGlyphOutline(j);
			Rectangle2D.union(textBounds, glyphs[j].getBounds2D(), textBounds);
		}
		return glyphs;
	}

	static List<Shape> drawCross(double retractionX0, double retractionY0, int m) {
		List<Shape> retractionContactPointShapes = ImmutableList.<Shape> of(new Line2D.Double(
				retractionX0 - m, retractionY0 - m, retractionX0 + m, retractionY0 + m),
				new Line2D.Double(retractionX0 - m, retractionY0 + m, retractionX0 + m,
						retractionY0 - m));
		return retractionContactPointShapes;
	}

	static List<Point2D.Double> createWlcPoints(double retractionX0,
			LinearFunction retractionEndLinearFunction, PlainPicoData plainPicoData,
			double horizontalDeflection, double contourLength, boolean endToEndDistance,
			double kbtOverP, double svgDefScale) {
		int t = 255;
		List<Point2D.Double> wlcPoints = new ArrayList<Point2D.Double>(t + 1);
		double overshoot = 1.05;
		for (int j = 0; j <= t; ++j) {
			double y = (double) j / t * horizontalDeflection * overshoot;
			double nonNegativeForceNewton = y * -plainPicoData.deflectionLsbToNewtonsFactor();
			assert nonNegativeForceNewton >= 0;
			double zMetres = endToEndDistance ? -Wlc.computeEndToEndDistance(
				kbtOverP, contourLength, nonNegativeForceNewton) : -Wlc
					.computeExtension(kbtOverP, plainPicoData.springConstantNPerM,
							contourLength, nonNegativeForceNewton);
			if (!Double.isNaN(zMetres)) {
				double zLsb = retractionX0 + zMetres / plainPicoData.zLsbToMetresFactor;
				wlcPoints.add(new Point2D.Double(zLsb, (y + retractionEndLinearFunction.evaluate(zLsb)) * svgDefScale));
			}
		}
		return wlcPoints;
	}

	static Line2D drawEmpiricalBaseline(DirectionData retraction,
			ZLsbDeflectionLsbPoint retractionContactPoint, double baselineSlopeZLsbDefLsb,
			double empiricalBaseline, double svgDefScale) {
		LinearFunction empiricalBaselineLinearFunction = new LinearFunction(
				baselineSlopeZLsbDefLsb * svgDefScale, empiricalBaseline + retractionContactPoint.deflectionLsb
						- baselineSlopeZLsbDefLsb * retractionContactPoint.zLsb);
		double x1 = retraction.z(0);
		double y1 = empiricalBaselineLinearFunction.evaluate(x1);
		double x2 = retraction.z(retraction.timeInstantCount() - 1);
		double y2 = empiricalBaselineLinearFunction.evaluate(x2);
		return new Line2D.Double(x1, y1 * svgDefScale, x2, y2 * svgDefScale);
	}

  public static List<Path2D.Float> createPaths(
    ShortList z, ShortList def, IntList jp, double defScale, short minZ, short minDef) {
    Path2D.Float[] paths = new Path2D.Float[jp.size() + 1];
    paths[0] = PicoShapes.createPath(z, def, defScale, 0, jp.getInt(0) + 1, minZ, minDef);
  	for (int i = 0; i < jp.size() - 1; ++i) {
      paths[i + 1] = PicoShapes.createPath(z, def, defScale, jp.getInt(i), jp.getInt(i + 1) + 1, minZ, minDef);
  	}
  	paths[jp.size()] = PicoShapes.createPath(z, def, defScale, jp.getInt(jp.size() - 1), z.size(), minZ, minDef);
    return Collections.unmodifiableList(Arrays.asList(paths));
  }

  public static Path2D.Float createPath(
  	ShortList z, ShortList def, double defScale, int from, int to, short minZ, short minDef) {
    Path2D.Float path = new Path2D.Float();
  	Preconditions.checkArgument(z.size() == def.size());
  	Preconditions.checkArgument(0 <= from & from <= to & to <= z.size());
  	if (from != to) {
  		path.moveTo(z.getShort(from) - minZ, (def.getShort(from) - minDef) * defScale);
  		for (int i = from + 1; i < to; ++i) {
  			path.lineTo(z.getShort(i) - minZ, (def.getShort(i) - minDef) * defScale);
  		}
  	}
  	return path;
  }

  /**
   * {@link http://stackoverflow.com/questions/3403826/how-to-dynamically-compute-a-list-of-colors}
   */
  public static List<Color> generateDifferentColors(int n) {
    Color[] cols = new Color[n];
    for (int i = 0; i < n; i++) {
        cols[i] = Color.getHSBColor((float) i / n, 1, 1);
    }
    return ImmutableList.copyOf(cols);
  }

  /**
   * The very first color is GREEN, for peaks or other things.
   * @param k number of colors
   */
  public static List<Color> colorsForPieces(int k) {
    final List<Color> colors;
    if (k <= PicoShapes.defaultCurrentAndSegmentColors.size()) {
      colors = PicoShapes.defaultCurrentAndSegmentColors.subList(0, k);
    } else {
      List<Color> generatedColors = generateDifferentColors(k - PicoShapes.defaultCurrentAndSegmentColors.size());
      colors = ImmutableList.copyOf(Iterables.concat(PicoShapes.defaultCurrentAndSegmentColors, generatedColors));
    }
    return colors;
  }

  public static List<Color> invert(List<Color> a) {
    List<Color> b = new ArrayList<>(a.size());
    for (Color c : a) {
      b.add(new Color(0xFFFFFF - c.getRGB()));
    }
    return b;
  }
}
